package game;

import static game.MarketPlace.getInstance;
import static game.Settings.getColumns;
import static game.Settings.getFactoriesNumber;
import static game.Settings.getLines;
import static game.Settings.loadBuildingsResources;
import static game.Settings.loadFactoriesInfo;
import static game.Settings.loadMap;
import static game.Settings.loadMines;
import static game.Strategy.getStrategy;
import static game.Types.ROCK;
import static game.Types.WOOD;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
import static utils.StrategyGameUtils.getInputWarehouse;
import static utils.StrategyGameUtils.getOutputWarehouse;
import static utils.StrategyGameUtils.getResourceInputTypeForFactory;
import static utils.StrategyGameUtils.getResourceOutputTypeForFactory;
import static utils.StrategyGameUtils.getFactory;
import static utils.StrategyGameUtils.getSymbol;
import static utils.StrategyGameUtils.hasBuiltAll;
import static utils.StrategyGameUtils.getMine;
import static utils.StrategyGameUtils.getPoint;
import static utils.StrategyGameUtils.getFreePointWithSymbol;
import static utils.StrategyGameUtils.getPointWithSymbol;
import static utils.StrategyGameUtils.getResourceWithType;
import static utils.StrategyGameUtils.getType;
import static utils.StrategyGameUtils.isFactory;
import static utils.StrategyGameUtils.printMap;
import static utils.StrategyGameUtils.stopAllFactories;
import static utils.StrategyGameUtils.getBasicType;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

import org.apache.commons.lang3.tuple.Pair;

import net.jcip.annotations.GuardedBy;

import exceptions.CanNotTradeException;
import exceptions.OutOfOrderException;

public class Player extends Thread {
	private static volatile boolean endOfGame = false;
	// Binary semaphore used to ensure that only one thread modifies a
	// PointOnMap at a time
	private static final Semaphore mapSemaphore = new Semaphore(1);
	@GuardedBy("mapSemaphore")
	private static List<PointOnMap> map = loadMap();
	private static final int linesNumber = getLines();
	private static final int columnsNumber = getColumns();
	private static final int factoriesNumber = getFactoriesNumber();
	private static final HashMap<Types, List<Pair<Types, Integer>>> buildingsResources = loadBuildingsResources();
	private static final HashMap<Types, Pair<Integer, Integer>> factoriesInfo = loadFactoriesInfo();
	private static final MarketPlace marketPlace = getInstance();
	private static final List<Mine> mines = loadMines();
	private final long TIMEOT_IN_MILLISECONDS = 5000;
	// Latch received from the main thread, countDown will be called if this
	// thread is the winner
	private static CountDownLatch winner;

	private List<Resource> resources;
	private List<Building> buildings;
	private List<Warehouse> warehouses;
	private Random generator;
	private int number;
	private int x, y;
	private int currentFactoriesNumber;

	public Player(int number, int x, int y, CountDownLatch latchWinner) {
		this.number = number;
		this.x = x;
		this.y = y;
		generator = new Random();
		buildings = new ArrayList<Building>();
		resources = new ArrayList<Resource>();
		warehouses = new ArrayList<Warehouse>();
		currentFactoriesNumber = 0;
		winner = latchWinner;
	}

	private void moveTo(int x, int y) {
		if (!Thread.currentThread().isInterrupted()) {
			long distance = (long) sqrt(pow(this.x - x, 2) + pow(this.y - y, 2));
			try {
				Thread.sleep(distance);
				mapSemaphore.acquire();
				PointOnMap currentPoint = getPoint(map, this.x, this.y);
				char currentSymbol = currentPoint.getSymbol();
				boolean mineFlag = (currentSymbol == 'M' || currentSymbol == 'G') ? true
						: false;
				PointOnMap nextPoint = getPoint(map, x, y);
				char symbol = nextPoint.getSymbol();
				switch (symbol) {
				case ' ':
					nextPoint.setSymbol('*');
					nextPoint.setOwner(number);
					System.out.println("Player " + number + " moved to (" + x
							+ "," + y + ")!");
					if (!mineFlag) {
						currentPoint.setOwner(0);
						currentPoint.setSymbol(' ');
					}
					mapSemaphore.release();
					this.x = x;
					this.y = y;
					break;
				case 'w':
					nextPoint.setSymbol(' ');
					if (!mineFlag) {
						currentPoint.setOwner(0);
						currentPoint.setSymbol(' ');
					}
					mapSemaphore.release();
					this.x = x;
					this.y = y;
					take(WOOD);
					break;
				case 'r':
					nextPoint.setSymbol(' ');
					if (!mineFlag) {
						currentPoint.setOwner(0);
						currentPoint.setSymbol(' ');
					}
					mapSemaphore.release();
					this.x = x;
					this.y = y;
					take(ROCK);
					break;
				case 'G':
				case 'M':
					if (!mineFlag) {
						currentPoint.setOwner(0);
						currentPoint.setSymbol(' ');
					}
					mapSemaphore.release();
					this.x = x;
					this.y = y;
					mine(TIMEOT_IN_MILLISECONDS, x, y);
					break;
				default:
					System.out.println("Player " + number
							+ " tried to move to (" + x + "," + y
							+ "), but this point isn't free!");
					mapSemaphore.release();
				}
			} catch (InterruptedException e) {
				stopAllFactories(buildings);
				Thread.currentThread().interrupt();
			}
		} else {
			stopAllFactories(buildings);
		}
	}

	private void mine(long timeInMiliseconds, int x, int y) {
		if (!Thread.currentThread().isInterrupted()) {
			Mine instanceMine = getMine(mines, x, y);
			try {
				Resource resource = instanceMine.mine(timeInMiliseconds);
				take(resource);
			} catch (OutOfOrderException e) {
				System.out.println("Player " + number + " can not mine:"
						+ e.getMessage());
				try {
					mapSemaphore.acquire();
					PointOnMap currentPoint = getPoint(map, x, y);
					currentPoint.setSymbol(' ');
					currentPoint.setOwner(0);
					mines.remove(instanceMine);
					mapSemaphore.release();
				} catch (InterruptedException e1) {
					stopAllFactories(buildings);
					Thread.currentThread().interrupt();
				}
			} catch (InterruptedException e) {
				stopAllFactories(buildings);
				Thread.currentThread().interrupt();
			}
		} else {
			stopAllFactories(buildings);
		}

	}

	private void take(Types type) {
		if (!Thread.currentThread().isInterrupted()) {
			Resource resource = getResourceWithType(resources, type);
			int quantity = generator.nextInt(20) + 1;
			if (resource == null) {
				resources.add(new Resource(type, quantity));
			} else {
				resource.increase(quantity);
			}
			System.out.println("Player " + number + " took " + quantity
					+ " pieces of " + type + "!");
		} else {
			stopAllFactories(buildings);
		}
	}

	private void take(Resource resource) {
		if (!Thread.currentThread().isInterrupted()) {
			Resource resourceWithSameType = getResourceWithType(resources,
					resource.getType());
			if (resourceWithSameType == null) {
				resources.add(resource);
			} else {
				resourceWithSameType.increase(resource.getCurrentQuantity());
			}
			System.out.println("Player " + number + " took "
					+ resource.getCurrentQuantity() + " pieces of "
					+ resource.getType() + "!");
		} else {
			stopAllFactories(buildings);
		}
	}

	private void exchange(Types neededType, int neededQuantity,
			Types notNeededType, int notNeededQuantity) {
		if (!Thread.currentThread().isInterrupted()) {
			Resource notNeededResource = getResourceWithType(resources,
					notNeededType);
			if (notNeededResource != null
					&& notNeededResource.getCurrentQuantity() >= notNeededQuantity) {
				try {
					Resource resource = marketPlace.exchange(neededType,
							neededQuantity, notNeededType, notNeededQuantity);
					int quantity = resource.getCurrentQuantity();
					take(resource);
					notNeededResource.decrease(quantity);
					System.out.println("Player " + number + " exchanged "
							+ quantity + " pieces of " + notNeededType
							+ " with " + quantity + " pieces of " + neededType);
				} catch (CanNotTradeException e) {
					System.out.println("Player " + number + " cannot trade:"
							+ e.getMessage());
				} catch (InterruptedException e) {
					stopAllFactories(buildings);
					Thread.currentThread().interrupt();
				}
			} else {
				System.out.println("Player " + number
						+ " can not trade because he doesn't have "
						+ notNeededQuantity + " of " + notNeededType + "!");
			}
		} else {
			stopAllFactories(buildings);
		}
	}

	private void build(Types buildingType, PointOnMap point) {
		if (!Thread.currentThread().isInterrupted()) {
			if (isFactory(buildingType)) {
				Pair<Integer, Integer> neededPair = factoriesInfo
						.get(buildingType);
				Warehouse inputBuffer = new Warehouse(neededPair.getLeft(),
						getResourceInputTypeForFactory(buildingType));
				Warehouse outputBuffer = new Warehouse(neededPair.getLeft(),
						getResourceOutputTypeForFactory(buildingType));
				warehouses.add(inputBuffer);
				warehouses.add(outputBuffer);
				Factory factory = new Factory(buildingType,
						neededPair.getLeft(), neededPair.getRight(),
						inputBuffer, outputBuffer);
				new Thread(factory).start();
				buildings.add(factory);
				currentFactoriesNumber++;
			} else {
				Building building = new Building(buildingType);
				buildings.add(building);
			}
			point.setOwner(number);
			System.out.println("Player " + number + " built a " + buildingType
					+ "!");
			if (hasBuiltAll(buildings)) {
				endOfGame = true;
				stopAllFactories(buildings);
				System.out.println("Player " + number + " won the game!");
				// System.out - the lock used by the println method
				synchronized (System.out) {
					printMap(map, columnsNumber);
				}
				winner.countDown();
			}
		} else {
			stopAllFactories(buildings);
		}
	}

	private Types missingResource(Types buildingType) {
		if (!Thread.currentThread().isInterrupted()) {
			Resource resource;
			int counter = 0;
			for (Pair<Types, Integer> currentPair : buildingsResources
					.get(buildingType)) {
				Types resourceType = currentPair.getLeft();
				if (!isFactory(buildingType)) {
					resourceType = getBasicType(resourceType);
				}
				resource = getResourceWithType(resources, resourceType);
				if (resource == null) {
					System.out.println("Player " + number + " cannot build a "
							+ buildingType + " because he doesn't have "
							+ resourceType + "!");
					return resourceType;
				}
				if (resource.getCurrentQuantity() < currentPair.getRight()) {
					System.out.println("Player " + number + " cannot build a "
							+ buildingType + " because he doesn't have enough "
							+ resourceType + "!");
					return resourceType;
				}
				counter++;
				if (counter == buildingsResources.get(buildingType).size())
					resource.decrease(currentPair.getRight());
			}
			System.out.println("Player " + number + " can build a "
					+ buildingType + "!");
		} else {
			stopAllFactories(buildings);
		}
		return null;
	}

	private void obtainFromFactory(Types factoryType, int quantity) {
		if (!Thread.currentThread().isInterrupted()) {
			provideInput(factoryType, quantity);
			takeOutput(factoryType, quantity);
		} else {
			stopAllFactories(buildings);
		}
	}

	private void provideInput(Types factoryType, int quantity) {
		Warehouse inputBuffer = getInputWarehouse(warehouses, factoryType);
		if (inputBuffer != null) {
			try {
				inputBuffer.addInput(quantity);
				System.out.println("Player " + number + " provided input to "
						+ factoryType + "!");
			} catch (InterruptedException e) {
				stopAllFactories(buildings);
				Thread.currentThread().interrupt();
			}
		}
	}

	private void takeOutput(Types factoryType, int quantity) {
		Warehouse outputBuffer = getOutputWarehouse(warehouses, factoryType);
		if (outputBuffer != null) {
			try {
				Resource resource = outputBuffer.takeOutput(quantity);
				if (resource != null) {
					take(resource);
					System.out.println("Player " + number
							+ " took output from " + factoryType + "!");
				}
			} catch (InterruptedException e) {
				stopAllFactories(buildings);
				Thread.currentThread().interrupt();
			}
		}
	}

	public void run() {
		String currentStrategy;
		String[] splitStrategy;
		int option;
		PointOnMap currentPoint, nextPoint;
		Types missingType;
		while (!endOfGame && !this.isInterrupted()) {
			currentStrategy = getStrategy();
			splitStrategy = currentStrategy.split(" ");
			if (splitStrategy[0].equals("moveTo")) {
				moveTo(generator.nextInt(linesNumber - 1) + 1,
						generator.nextInt(columnsNumber - 1) + 1);
				continue;
			}
			if (splitStrategy[0].equals("build")) {
				Types buildingType = getType(splitStrategy[1]);
				if (!isFactory(buildingType)
						&& currentFactoriesNumber != factoriesNumber) {
					continue;
				}
				nextPoint = getFreePointWithSymbol(map, this.number,
						getSymbol(buildingType));
				if (nextPoint == null) { // there is no space on the map
					continue;
				}
				// obtain the needed basic resources
				while ((missingType = missingResource(buildingType)) != null) {
					currentPoint = getPointWithSymbol(map,
							getSymbol(missingType));
					if (currentPoint != null) {
						moveTo(currentPoint.getX(), currentPoint.getY());
					}
				}
				if (isFactory(buildingType)) {
					build(buildingType, nextPoint);
					continue;
				}
				option = generator.nextInt(2);
				for (Pair<Types, Integer> currentPair : buildingsResources
						.get(buildingType)) {
					switch (option) {
					case 0:
						exchange(currentPair.getLeft(), currentPair.getRight(),
								getBasicType(currentPair.getLeft()),
								currentPair.getRight());
						break;
					case 1:
						obtainFromFactory(
								getFactory(getBasicType(currentPair.getLeft())),
								currentPair.getRight());
						break;
					}
				}
				build(buildingType, nextPoint);
			}
		}
		stopAllFactories(buildings);
	}
}
